package parsetree;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkPositionIndex;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.text.BadLocationException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlList;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import jaxb.metagrammar.OperatorToUUIDStringAdapter;
import metagrammar.Operator;
import metagrammar.Type;
import metagrammar.Typed;

import com.google.common.collect.ImmutableList;

/**
 * An OperatorSpec provides the information needed to parse a {@linkplain OperatorElement node} from a string of text.
 * The fist terminal string for the node is provided by {@link #getLiteral()}. The literal's position relative
 * to the non-terminal children is provided by {@link #getLiteralBeforePosition()} ie: if the literal is before
 * position <code>n</code> then that means that the fully parsed subtree rooted at an {@linkplain OperatorElement}
 * generated from this should wither have at least <code>n</code> children of the form 
 * <blockquote><code>
 * E1 E2 ... En-1 x En
 * </code></blockquote>
 * or <code>n-1</code> children of the form
 * <blockquote><code>
 * E1 E2 ... En-1 x
 * </code></blockquote>
 * where <code>Ei</code> is a {@linkplain Typed} Element such that
 * <code>Ei.{@link Typed#getType() getType()} == {@link #getCorrectedInputTuple() getCorrectedInputTuple()}[i].getType()</code>
 * and <code>x</code> is some representative of a String such that <code>x.equals({@link #getLiteral()})</code>.
 * 
 * 
 * @author Joshua Lockerman
 *
 */
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class OperatorSpec implements ElementFactory, Serializable, Externalizable {
    
    private class OperatorSpecWithPrecedence extends OperatorSpec {
        
        private final float precedence;
        
        public OperatorSpecWithPrecedence(float precedence)
        {
            this.precedence = precedence;
        }
        
        @Override
        public OperatorElement generateElement(BranchElement parent, int operatorStart,
                int operatorEnd) throws BadLocationException
                {
            return OperatorSpec.this.generateElement(parent, operatorStart, operatorEnd);
                }
        
        @Override
        public List<Type> getCorrectedInputTuple()
        {
            return OperatorSpec.this.getCorrectedInputTuple();
        }
        
        @Override
        public int[] getCorrectedInputTupleOrder()
        {
            return OperatorSpec.this.getCorrectedInputTupleOrder();
        }
        
        @Override
        public String getLiteral()
        {
            return OperatorSpec.this.getLiteral();
        }
        
        @Override
        public int getLiteralBeforePosition()
        {
            return OperatorSpec.this.getLiteralBeforePosition();
        }
        
        @Override
        public Operator getOperator()
        {
            return OperatorSpec.this.getOperator();
        }
        
        @Override
        public OperatorSpec withPrecedence(float precedence)
        {
            return OperatorSpec.this.withPrecedence(precedence);
        }
        
        @Override
        public float getPrecedence()
        {
            return precedence;
        }
    }
    
    @XmlJavaTypeAdapter(OperatorToUUIDStringAdapter.class)
    @XmlAttribute
    private Operator operator;
    @XmlList
    private int[] correctedOrder;
    
    private String literal;
    private int literalBeforePosition;
    
    public OperatorSpec(Operator operator, int[] correctedOrder, String literal,
            int literalBeforePosition)
    {
        checkPositionIndex(literalBeforePosition, operator.getInputTypes().size() + 1);
        this.operator = operator;
        if(correctedOrder == null) {
            correctedOrder = new int[operator.getInputTypes().size()];
            for(int i = 0; i < correctedOrder.length; i++) {
                correctedOrder[i] = i;
            }
            
        }
        else {
            checkArgument(correctedOrder.length == operator.getInputTypes().size());
            boolean[] positionUsed = new boolean[correctedOrder.length];
            for(int i = 0; i < correctedOrder.length; i++) {
                checkPositionIndex(correctedOrder[i], operator.getInputTypes().size());
                checkArgument( !positionUsed[correctedOrder[i]]);
                positionUsed[correctedOrder[i]] = true;
            }
            correctedOrder = Arrays.copyOf(correctedOrder, correctedOrder.length);
        }
        this.correctedOrder = correctedOrder;
        this.literal = literal;
        this.literalBeforePosition = literalBeforePosition;
    }
    
    public OperatorSpec(Operator operator, String literal, int literalBeforePosition)
    {
        this(operator, null, literal, literalBeforePosition);
    }
    
    /**
     * For serialization and deserialization only. This constructor will not result in
     * a valid OperatorSpec.
     */
    protected OperatorSpec()
    {
    }
    
    /* (non-Javadoc)
     * @see parsetree.OperatorElementFactory#generateOperatorElement(parsetree.BranchElement, int, int)
     */
    @Override
    public OperatorElement generateElement(BranchElement parent, int operatorStart,
            int operatorEnd) throws BadLocationException
            {
        return new DefaultOperatorElement(parent, this, operatorStart, operatorEnd);
            }
    
    public List<Type> getCorrectedInputTuple()
    {
        ImmutableList<Type> input = operator.getInputTypes();
        ArrayList<Type> correctedInput = new ArrayList<Type>();
        getCorrectedInputTupleOrder();
        for(int i = 0; i < correctedOrder.length; i++) {
            correctedInput.add(input.get(correctedOrder[i]));
        }
        return correctedInput;
    }
    
    /**
     * The corrected order is an array where ar[i] means that the Child of the 
     * OperatorElement at position i should correspond to the Type at the 
     * Operator's {@link Operator#getInputTypes() inputTuple} position ar[i].
     * @return an array which can be used to map from the list of typed children
     * of an {@link OperatorElement} to their actual positions in the
     * {@link Operator#getInputTypes() inputTuple} of the underlying {@link Operator#}
     * @see {@link #getCorrectedInputTuple()}
     */
    public int[] getCorrectedInputTupleOrder()
    {
        return Arrays.copyOf(correctedOrder, correctedOrder.length);
    }
    
    public String getLiteral()
    {
        return literal;
    }
    
    /**
     * @return the position in the Operator's input tuple
     * before which the literal should appear.
     */
    public int getLiteralBeforePosition()
    {
        return literalBeforePosition;
    }
    
    public Operator getOperator()
    {
        return this.operator;
    }
    
    public float getPrecedence()
    {
        return 0;
    }
    
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj)
    {
        if(this == obj) {
            return true;
        }
        if(obj == null) {
            return false;
        }
        if( !(obj instanceof OperatorSpec)) {
            return false;
        }
        OperatorSpec other = (OperatorSpec) obj;
        if( !Arrays.equals(correctedOrder, other.correctedOrder)) {
            return false;
        }
        if(literal == null) {
            if(other.literal != null) {
                return false;
            }
        }
        else if( !literal.equals(other.literal)) {
            return false;
        }
        if(literalBeforePosition != other.literalBeforePosition) {
            return false;
        }
        if(operator == null) {
            if(other.operator != null) {
                return false;
            }
        }
        else if( !operator.equals(other.operator)) {
            return false;
        }
        return true;
    }
    
    public OperatorSpec withPrecedence(float precedence)
    {
        return new OperatorSpecWithPrecedence(precedence);
    }
    
    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + Arrays.hashCode(correctedOrder);
        result = prime * result + ((literal == null) ? 0 : literal.hashCode());
        result = prime * result + literalBeforePosition;
        result = prime * result + ((operator == null) ? 0 : operator.hashCode());
        return result;
    }
    
    @Override
    public String toString()
    {
        StringBuilder builder = new StringBuilder();
        for(int i = 0; i < literalBeforePosition; i++) {
            builder.append('◌');
        }
        builder.append(literal);
        for(int i = literalBeforePosition; i < correctedOrder.length; i++) {
            builder.append('◌');
        }
        return builder.toString();
    }
    
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
    {
        operator = (Operator) in.readObject();
        literal = (String) in.readObject();
        literalBeforePosition = in.readInt();
    }
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException
    {
        out.writeObject(operator);
        out.writeObject(literal);
        out.writeInt(literalBeforePosition);
    }
}